/*
 *  Copyright (C) 2020 the original author or authors.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package we.fizz;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;

import com.alibaba.nacos.api.config.annotation.NacosValue;
import we.config.AppConfigProperties;
import we.fizz.input.ClientInputConfig;
import we.fizz.input.Input;
import we.fizz.input.InputType;

import org.apache.commons.io.FileUtils;
import org.noear.snack.ONode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

import static we.listener.AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE;
import static we.util.Constants.Symbol.FORWARD_SLASH;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 
 * @author francis
 * @author zhongjie
 *
 */
@Component
public class ConfigLoader {
	/**
	 * 聚合配置存放Hash的Key
	 */
	private static final String AGGREGATE_HASH_KEY = "fizz_aggregate_config";
	
	private static Map<String, String> aggregateResources = null;
	private static Map<String, ConfigInfo> resourceKey2ConfigInfoMap = null;
	private static Map<String, String> aggregateId2ResourceKeyMap = null;
	
	@Resource
	private AppConfigProperties appConfigProperties;
	@Resource(name = AGGREGATE_REACTIVE_REDIS_TEMPLATE)
	private ReactiveStringRedisTemplate reactiveStringRedisTemplate;

	@NacosValue(value = "${fizz.aggregate.read-local-config-flag:false}", autoRefreshed = true)
	@Value("${fizz.aggregate.read-local-config-flag:false}")
	private Boolean readLocalConfigFlag;

	public Input createInput(String configStr) throws IOException {
		ONode cfgNode = ONode.loadStr(configStr);

		Input input = new Input();
		input.setName(cfgNode.select("$.name").getString());

		ClientInputConfig clientInputConfig = new ClientInputConfig();
		clientInputConfig.setDataMapping(cfgNode.select("$.dataMapping").toObject(Map.class));
		clientInputConfig.setHeaders(cfgNode.select("$.headers").toObject(Map.class));
		clientInputConfig.setMethod(cfgNode.select("$.method").getString());
		clientInputConfig.setPath(cfgNode.select("$.path").getString());
		if(clientInputConfig.getPath().startsWith(TEST_PATH_PREFIX)) {
			// always enable debug for testing
			clientInputConfig.setDebug(true);
		}else {
			if(cfgNode.select("$.debug") != null) {
				clientInputConfig.setDebug(cfgNode.select("$.debug").getBoolean());
			}
		}
		clientInputConfig.setType(InputType.valueOf(cfgNode.select("$.type").getString()));
		clientInputConfig.setLangDef(cfgNode.select("$.langDef").toObject(Map.class));
		clientInputConfig.setBodyDef(cfgNode.select("$.bodyDef").toObject(Map.class));
		clientInputConfig.setHeadersDef(cfgNode.select("$.headersDef").toObject(Map.class));
		clientInputConfig.setParamsDef(cfgNode.select("$.paramsDef").toObject(Map.class));
		clientInputConfig.setScriptValidate(cfgNode.select("$.scriptValidate").toObject(Map.class));
		clientInputConfig.setValidateResponse(cfgNode.select("$.validateResponse").toObject(Map.class));
		input.setConfig(clientInputConfig);
		return input;
	}

	public Pipeline createPipeline(String configStr) throws IOException {
		ONode cfgNode = ONode.loadStr(configStr);
		Pipeline pipeline = new Pipeline();

		List<Map<String, Object>> stepConfigs = cfgNode.select("$.stepConfigs").toObject(List.class);
		for (Map<String, Object> stepConfig : stepConfigs) {
			// set the specified env URL
			this.handleRequestURL(stepConfig);
			
			Step step = new Step.Builder().read(stepConfig);
			step.setName((String) stepConfig.get("name"));
			if (stepConfig.get("stop") != null) {
				step.setStop((Boolean) stepConfig.get("stop"));
			} else {
				step.setStop(false);
			}
			step.setDataMapping((Map<String, Object>) stepConfig.get("dataMapping"));
			pipeline.addStep(step);
		}

		return pipeline;
	}

	public List<ConfigInfo> getConfigInfo() {
		if (aggregateResources == null) {
			try {
				this.init();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return new ArrayList<>(resourceKey2ConfigInfoMap.values());
	}

	public String getConfigStr(String configId) {
		if (aggregateResources == null) {
			try {
				this.init();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		String resourceKey = aggregateId2ResourceKeyMap.get(configId);
		if (resourceKey == null) {
			return null;
		}
		return aggregateResources.get(resourceKey);
	}
	
	private void handleRequestURL(Map<String, Object> stepConfig) {
		List<Object> requests = (List<Object>) stepConfig.get("requests");
		for (Object obj : requests) {
			Map<String, Object> request = (Map<String, Object>) obj;
			String envUrl = (String) request.get(appConfigProperties.getEnv() + "Url");
			if(!StringUtils.isEmpty(envUrl)) {				
				request.put("url", request.get(appConfigProperties.getEnv() + "Url"));
			}
		}
	}

	public synchronized void init() throws Exception {
		if (aggregateResources == null) {
			aggregateResources = new ConcurrentHashMap<>(1024);
			resourceKey2ConfigInfoMap =  new ConcurrentHashMap<>(1024);
			aggregateId2ResourceKeyMap = new ConcurrentHashMap<>(1024);
		}

		if (readLocalConfigFlag) {
			File dir = new File("json");
			if (dir.exists() && dir.isDirectory()) {
				File[] files = dir.listFiles();
				if (files != null && files.length > 0) {
					for (File file : files) {
						if (!file.exists()) {
							throw new IOException("File not found");
						}
						String configStr = FileUtils.readFileToString(file, Charset.forName("UTF-8"));
						this.addConfig(configStr);
					}
				}
			}
		} else {
			// 从Redis缓存中获取配置
			reactiveStringRedisTemplate.opsForHash().scan(AGGREGATE_HASH_KEY).subscribe(entry -> {
				String configStr = (String) entry.getValue();
				this.addConfig(configStr);
			});
		}
	}

	public synchronized void addConfig(String configStr) {
		if (aggregateResources == null) {
			try {
				this.init();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		ONode cfgNode = ONode.loadStr(configStr);
		String method = cfgNode.select("$.method").getString();
		String path = cfgNode.select("$.path").getString();
		String resourceKey = method.toUpperCase() + ":" + path;
		String configId = cfgNode.select("$.id").getString();
		String configName = cfgNode.select("$.name").getString();
		long version = cfgNode.select("$.version").getLong();
		if (StringUtils.hasText(configId)) {
			String existResourceKey = aggregateId2ResourceKeyMap.get(configId);
			if (StringUtils.hasText(existResourceKey)) {
				// 删除旧有的配置
				aggregateResources.remove(existResourceKey);
				resourceKey2ConfigInfoMap.remove(existResourceKey);
			}
			aggregateId2ResourceKeyMap.put(configId, resourceKey);
		}
		aggregateResources.put(resourceKey, configStr);
		resourceKey2ConfigInfoMap.put(resourceKey, this.buildConfigInfo(configId, configName, method, path, version));
	}

	public synchronized void deleteConfig(String configIds) {
		if (CollectionUtils.isEmpty(aggregateId2ResourceKeyMap)) {
			return;
		}

		JSONArray idArray = JSON.parseArray(configIds);
		idArray.forEach(it -> {
			String configId = (String) it;
			String existResourceKey =aggregateId2ResourceKeyMap.get(configId);
			if (StringUtils.hasText(existResourceKey)) {
				aggregateResources.remove(existResourceKey);
				resourceKey2ConfigInfoMap.remove(existResourceKey);
				aggregateId2ResourceKeyMap.remove(configId);
			}
		});
	}

	public AggregateResource matchAggregateResource(String method, String path) {
		if (aggregateResources == null) {
			try {
				init();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}
		String key = method.toUpperCase() + ":" + path;
		if(aggregateResources.containsKey(key) && aggregateResources.get(key) != null) {
			String configStr = aggregateResources.get(key);
			Input input = null;
			Pipeline pipeline = null;
			try {
				input = createInput(configStr);
				pipeline = createPipeline(configStr);
			} catch (IOException e) {
				e.printStackTrace();
				return null;
			}
			if (pipeline != null && input != null) {
				ClientInputConfig cfg = (ClientInputConfig) input.getConfig();
				return new AggregateResource(pipeline, input);
			}
		}
		return null;
	}
	private ConfigInfo buildConfigInfo(String configId, String configName, String method, String path, long version) {
		String serviceName = this.extractServiceName(path);
		ConfigInfo configInfo = new ConfigInfo();
		configInfo.setConfigId(configId);
		configInfo.setConfigName(configName);
		configInfo.setServiceName(serviceName);
		configInfo.setMethod(method);
		configInfo.setPath(path);
		configInfo.setVersion(version == 0 ? null : version);
		return configInfo;
	}

    private static final String FORMAL_PATH_PREFIX = "/proxy/";
    private static final int FORMAL_PATH_SERVICE_NAME_START_INDEX = 7;
    private static final String TEST_PATH_PREFIX = "/proxytest/";
    private static final int TEST_PATH_SERVICE_NAME_START_INDEX = 11;
	private String extractServiceName(String path) {
        if (path != null) {
            if (path.startsWith(FORMAL_PATH_PREFIX)) {
                int endIndex = path.indexOf(FORWARD_SLASH, FORMAL_PATH_SERVICE_NAME_START_INDEX);
                if (endIndex > FORMAL_PATH_SERVICE_NAME_START_INDEX) {
                    return path.substring(FORMAL_PATH_SERVICE_NAME_START_INDEX, endIndex);
                }
            } else if (path.startsWith(TEST_PATH_PREFIX)) {
                int endIndex = path.indexOf(FORWARD_SLASH, TEST_PATH_SERVICE_NAME_START_INDEX);
                if (endIndex > TEST_PATH_SERVICE_NAME_START_INDEX) {
                    return path.substring(TEST_PATH_SERVICE_NAME_START_INDEX, endIndex);
                }
            }
        }
        return null;
    }

	public static class ConfigInfo implements Serializable {
		private static final long serialVersionUID = 1L;
		/**
		 * 配置ID
		 */
		private String configId;

		/**
		 * 配置名
		 */
		private String configName;

		/**
		 * 服务名
		 */
		private String serviceName;
		/**
		 * 接口请求method类型
		 */
		private String method;
		/**
		 * 接口请求路径
		 */
		private String path;
		/**
		 * 版本号
		 */
		private Long version;

		@Override
		public boolean equals(Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			ConfigInfo that = (ConfigInfo) o;
			return Objects.equals(configId, that.configId) &&
					Objects.equals(configName, that.configName) &&
					Objects.equals(serviceName, that.serviceName) &&
					Objects.equals(method, that.method) &&
					Objects.equals(path, that.path) &&
					Objects.equals(version, that.version);
		}

		@Override
		public int hashCode() {
			return Objects.hash(configId, configName, serviceName, method, path, version);
		}

		public String getConfigId() {
			return configId;
		}

		public void setConfigId(String configId) {
			this.configId = configId;
		}

		public String getConfigName() {
			return configName;
		}

		public void setConfigName(String configName) {
			this.configName = configName;
		}

		public String getServiceName() {
			return serviceName;
		}

		public void setServiceName(String serviceName) {
			this.serviceName = serviceName;
		}

		public String getMethod() {
			return method;
		}

		public void setMethod(String method) {
			this.method = method;
		}

		public String getPath() {
			return path;
		}

		public void setPath(String path) {
			this.path = path;
		}

		public Long getVersion() {
			return version;
		}

		public void setVersion(Long version) {
			this.version = version;
		}
	}
}
